package goben

import (
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"gitee.com/zhangycsz/goben/util"
)

// 上传文件的配置文件
type UploadConfig struct {
	IsDirDate bool   //是否按日期存储目录,默认是false
	Url       string //http访问的前缀地址，比如:/upload 后面是文件名称
	Path      string //本地存储地址,比如 ./html/upload
	Size      int64  // 0:限制，其他是字节
	IsRepeat  bool   //是否允许重复上传,true:覆盖,false:不覆盖
	Type      string // 文件类型,支持多个,分割,比如:,apk,zip,image,video
	Ext       string //文件后缀,支持多个,分割,比如:.jpg,.png
}

// 上传是否成功的信息
type InMessage struct {
	Finsh   bool   `json:"finsh"`    //是否完成上传，如果是分片上传，则为false，否则为true
	Status  bool   `json:"status"`   //true:成功，false:失败
	Message string `json:"message"`  //错误信息
	Title   string `json:"title"`    //文件标题,带文件后缀
	Path    string `json:"path"`     //相对路径,除去根路径的路径
	ShowUrl string `json:"show_url"` //http访问的路径文件路径
	Md5     string `json:"md5"`      //文件MD5值
	Size    int64  `json:"size"`     //文件大小
	Ext     string `json:"ext"`      //文件后缀,不带.
	Name    string `json:"name"`     //文件名,不带后缀
}

// 返回到页面信息
type OutMessage struct {
	Result  string `json:"result"`  // ok:成功，failed:失败
	Status  bool   `json:"status"`  //true:成功，false:失败
	Message string `json:"message"` //错误信息
	Title   string `json:"title"`   //文件标题
	Path    string `json:"path"`    //http访问的路径文件路径
}

// 考虑到分片上传文件，在合并文件的时候，需要长时间占用时间，所以需要回调方式
type UploadCallBackFunStatus func(in *InMessage, err error)

// 操作上传文件
type UploadFile struct {
	Config              *UploadConfig
	TmpPath             string                  //临时目录，用于存放分片上传的文件片段
	UpCallBackFunStatus UploadCallBackFunStatus //分片上传文件合并时候的回调函数
}

func NewUploadFile() *UploadFile {
	return &UploadFile{}
}

// 得到文件后缀
func (ths *UploadFile) GetFileExtension(filename string) string {
	ext := filepath.Ext(filename)
	if ext == "" {
		if strings.LastIndex(filename, ".") > 0 {
			return filename[strings.LastIndex(filename, "."):]
		}
		return ""
	}
	// 处理类似 .tar.gz 的情况
	for {
		newExt := filepath.Ext(strings.TrimSuffix(filename, ext))
		if newExt == "" {
			break
		}
		ext = newExt + ext
	}
	return strings.TrimPrefix(ext, ".")
}

// 得到文件不带后缀的名称
func (ths *UploadFile) GetFileName(filename string) string {
	if strings.LastIndex(filename, ".") > 0 {
		return filename[:strings.LastIndex(filename, ".")]
	}
	return filename
}

// 没有切片直接上传文件
func (ths *UploadFile) UploadSize(name string, file *multipart.FileHeader) (in *InMessage, out OutMessage, err error) {
	in = &InMessage{}
	out = OutMessage{}
	ext := ths.GetFileExtension(name)
	fileName := ths.GetFileName(name)
	in.Name = fileName
	in.Ext = ext
	in.Title = name
	out.Title = name
	uuid := util.GetRandDateUUID(32)
	out.Result = "failed"
	out.Message = "上传失败"
	//保存的目录
	fileDir := "" //文件目录
	if ths.Config.IsDirDate {
		fileDir = "/" + time.Now().Format("2006-01-02")
	}
	relativePath := fileDir + "/" + uuid + "." + ext
	in.ShowUrl = ths.Config.Url + relativePath //http访问的地址
	out.Path = in.ShowUrl
	in.Path = ths.Config.Path + relativePath

	dstDir := ths.Config.Path + fileDir //本地目录地址
	if !CheckDirExists(dstDir) {
		Mkdir(dstDir)
	}
	in.Path = ths.Config.Path + relativePath
	if CheckDirExists(in.Path) { //文件存在
		in.Size = util.GetFileSize(in.Path)
		in.Md5 = util.GetFileMd5(in.Path)
		if !ths.Config.IsRepeat {
			in.Finsh = true
			in.Status = true
			in.Message = "文件已存在"
			out.Result = "ok"
			out.Status = true
			out.Message = "上传成功"
			if ths.UpCallBackFunStatus != nil {
				ths.UpCallBackFunStatus(in, nil)
			}
			return
		}
		err = os.Remove(in.Path) //删除文件
		if err != nil {
			return
		}
	}
	srcFile, err := file.Open()
	if err != nil {
		return
	}
	defer srcFile.Close()

	outFile, err := os.Create(in.Path)
	if err != nil {
		return
	}
	defer outFile.Close()
	_, err = io.Copy(outFile, srcFile)
	if err != nil {
		return
	}
	//有设置上传文件类型限制的，检查文件类型是否符合要求
	if ths.Config.Type != "" {

	}
	_size := util.GetFileSize(in.Path)
	if ths.Config.Size != 0 {
		if _size > ths.Config.Size {
			err = errors.New("文件太大")
			in.Finsh = true
			in.Status = false
			in.Message = "文件太大"
			out.Result = "failed"
			out.Message = "文件太大"
			out.Status = false
			out.Title = name
			os.Remove(in.Path) //删除文件
			if ths.UpCallBackFunStatus != nil {
				ths.UpCallBackFunStatus(in, err)
			}
			return
		}
	}
	in.Md5 = util.GetFileMd5(in.Path)
	in.Size = _size
	in.Finsh = true
	in.Status = true
	in.Message = "上传成功"
	out.Result = "ok"
	out.Status = true
	out.Message = "上传成功"
	out.Title = name
	if ths.UpCallBackFunStatus != nil {
		ths.UpCallBackFunStatus(in, error(nil))
	}
	return
}

// saveDir:保存的目录 checkExit:检查文件是否存在 fileSize:大小限制 默认是10M
func (ths *UploadFile) UploadSlice(name, uuid string, chunk, chunks int, file *multipart.FileHeader) (in *InMessage, out OutMessage, err error) {
	in = &InMessage{}
	ext := ths.GetFileExtension(name)
	fileName := ths.GetFileName(name)
	in.Name = fileName
	in.Ext = ext
	in.Title = name
	in.Finsh = false
	in.Status = false
	out.Result = "failed"
	out.Message = "上传失败"
	out.Title = name
	out.Status = false

	srcFile, err := file.Open()
	if err != nil {
		return
	}
	defer srcFile.Close()

	tempPath := ths.TmpPath + "/" + uuid
	tempPathName := tempPath + "/" + fmt.Sprintf("%d", chunk)
	//创建临时文件夹
	if !CheckDirExists(tempPath) {
		Mkdir(tempPath)
	}
	//文件已经存在
	if !CheckDirExists(tempPathName) {
		oldFile := tempPathName + ".temp"
		tempFile, err := os.Create(oldFile)
		if err != nil {
			return in, out, err
		}
		_, err = io.Copy(tempFile, srcFile)
		tempFile.Close()
		if err != nil {
			return in, out, err
		}
		err = os.Rename(oldFile, tempPathName)
		if err != nil {
			return in, out, err
		}
	}

	sortList, checkFinsh := ths.checkFinsh(tempPath, chunks)
	if !checkFinsh {
		//没完成
		out.Status = true
		out.Result = "ok"
		out.Message = "上传成功"
		return
	}
	uuid = util.GetRandDateUUID(32)
	fileDir := "" //文件目录
	if ths.Config.IsDirDate {
		fileDir = "/" + time.Now().Format("2006-01-02")
	}
	fileSavePath := "/" + uuid + "." + ext
	relativePath := fileDir + fileSavePath
	in.Path = ths.Config.Path + relativePath
	in.ShowUrl = ths.Config.Url + relativePath //输出显示的路径
	out.Path = ths.Config.Url + relativePath   //输出显示的路径
	dstDir := ths.Config.Path + fileDir        //本地目录地址
	if !CheckDirExists(dstDir) {
		Mkdir(dstDir)
	}
	if ths.UpCallBackFunStatus != nil {
		//异步合成文件
		go ths.compositFile(tempPath, sortList, in)
	} else {
		err = ths.compositFile(tempPath, sortList, in)
	}
	in.Finsh = true
	in.Status = true
	in.Message = "上传成功"
	in.Status = true
	out.Result = "ok"
	out.Message = "上传成功"
	return
}

// 判断文件是否完整，主要是文件夹里面的文件数量和总数是否一致
func (ths *UploadFile) checkFinsh(tempPath string, chunks int) (sortList []int, ret bool) {
	ret = false
	sortList = make([]int, 0)
	fileList, err := os.ReadDir(tempPath)
	if err != nil {
		return
	}
	for _, file := range fileList {
		//过滤临时文件
		if strings.LastIndex(file.Name(), ".temp") >= 0 {
			continue
		}
		id, _ := strconv.Atoi(file.Name())
		sortList = append(sortList, id)
	}

	sort.Ints(sortList)
	if len(sortList) == chunks {
		ret = true
	}
	return
}

// 文件合成,异步合成文件
func (ths *UploadFile) compositFile(tempPath string, sortList []int, in *InMessage) (err error) {
	outFile, err := os.Create(in.Path)
	if err != nil {
		return
	}
	defer outFile.Close()

	//所有的文件合并
	for _, file := range sortList {
		opath := tempPath + "/" + fmt.Sprintf("%d", file)
		src, err := os.Open(opath)
		if err != nil {
			break
		}
		io.Copy(outFile, src)
		src.Close()
		os.Remove(opath)
	}
	in.Size = util.GetFileSize(in.Path)
	in.Md5 = util.GetFileMd5(in.Path)
	in.Status = true
	in.Finsh = true
	in.Message = "上传成功"
	os.RemoveAll(tempPath) //删除临时文件夹
	if ths.UpCallBackFunStatus != nil {
		ths.UpCallBackFunStatus(in, err)
	}
	return
}

// 判断文件或者文件夹是否存在
func CheckDirExists(pathDir string) bool {
	_, er := os.Stat(pathDir)
	b := er == nil || os.IsExist(er)
	return b
}

func Mkdir(dir string) (e error) {
	_, er := os.Stat(dir)
	b := er == nil || os.IsExist(er)
	if !b {
		if err := os.MkdirAll(dir, 0775); err != nil {
			if os.IsPermission(err) {
				e = err
			}
		}
	}
	return
}
